﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Soul.Expressions
{
    public class Lambda
    {
        public static LambdaExpression Build(string token, Action<LambdaOptions> configure)
        {
            var options = new LambdaOptions();
            configure(options);
            var context = new LambdaContext(options);
            var body = Watch(context.Init(token), context);
            return Expression.Lambda(body, options.Parameters);
        }

        public static LambdaExpression Build<T>(string token, Action<LambdaOptions> configure)
        {
            var options = new LambdaOptions();
            configure(options);
            var context = new LambdaContext(options);
            var body = Watch(context.Init(token), context);
            if (body.Type != typeof(T))
            {
                body = Expression.Convert(body, typeof(T));
            }
            return Expression.Lambda(body, options.Parameters);
        }

        public static Expression Watch(string token, LambdaContext context)
        {
            if (context.TryGet(token, out var expression))
            {
                return expression;
            }
            else if (LambdaParser.TryConstant(token, out var constantExpression))
            {
                context.AddToken(constantExpression);
                return constantExpression;
            }
            else if (context.TryGetParameter(token, out var parameterExpression))
            {
                context.AddToken(parameterExpression);
                return parameterExpression;
            }
            else if (LambdaParser.TryNot(token, out string notExpre))
            {
                var expr = Watch(notExpre, context);
                return Expression.MakeUnary(ExpressionType.Not, expr, expr.Type);
            }
            else if (LambdaParser.TryMember(token, out MemberAccessToken memberAccessToken))
            {
                var expr = Watch(memberAccessToken.Own, context);
                var member = expr.Type.GetMember(memberAccessToken.Prop).FirstOrDefault();
                var key = context.AddToken(Expression.MakeMemberAccess(expr, member));
                token = token.Replace(memberAccessToken.Token, key);
                return Watch(token, context);
            }
            else if (LambdaParser.TryCall(token, out MethodCallToken methodCallToken))
            {
                var argumens = methodCallToken.Arguments.Select(s => Watch(s, context)).ToArray();
                var method = FindFunction(methodCallToken.Name, argumens, context.Options.Methods);
                var callExpression = Expression.Call(method, argumens);
                var key = context.AddToken(callExpression);
                token = token.Replace(methodCallToken.Token, key);
                return Watch(token, context);
            }
            else if (LambdaParser.TryUnary(token, out UnaryToken unaryToken))
            {
                var expr = Watch(unaryToken.Expr, context);
                var key = context.AddToken(expr);
                token = token.Replace(unaryToken.Token, key);
                return Watch(token, context);
            }
            else if (LambdaParser.TryBinary(token, out var binaryToken))
            {
                var type = binaryToken.GetExpressionType();
                var left = Watch(binaryToken.Left, context);
                var right = Watch(binaryToken.Right, context);
                LambdaConvert.Exchange(ref left, ref right);
                var binaryExpression = Expression.MakeBinary(type, left, right);
                var key = context.AddToken(binaryExpression);
                token = token.Replace(binaryToken.Token, key);
                return Watch(token, context);
            }
            throw new NotSupportedException(token);
        }

        public static MethodInfo FindFunction(string name, Expression[] arguments, IEnumerable<MethodInfo> methods)
        {
            if (methods == null)
            {
                throw new MissingMethodException(name);
            }
            var method = methods.Where(a => a.Name == name)
                .Where(a =>
                {
                    var parameters = a.GetParameters();
                    if (parameters.Length != arguments.Length)
                    {
                        return false;
                    }
                    for (int i = 0; i < arguments.Length; i++)
                    {
                        if (arguments[i].Type != parameters[i].ParameterType)
                        {
                            return false;
                        }
                    }
                    return true;
                })
                .FirstOrDefault();
            if (method == null)
            {
                throw new MissingMethodException($"add({string.Join(",", arguments.Select(s => s.Type.Name))})");
            }
            return method;
        }
    }
}
